//
// Created by jk101 on 2023/12/7.
//
#include "FFPlayer.h"
#include "../ggl.h"
#include <pthread.h>
#include <thread>
#include <cstdarg>
#include <unistd.h>
#include "../utils/BlockQueue.h"
#include "../Section902.cpp"

#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavutil/imgutils.h>
#include <libavutil/avutil.h>
#include <libavutil/log.h>
#include <libswscale/swscale.h>
#include <libavcodec/jni.h>
#include <libswresample/swresample.h>
#include <chrono>
#ifdef __cplusplus
} // endof extern "C"
#endif

class FFPlayerImpl : public FFPlayer {
/**
 * 实现头文件中声明的函数
 */
public:
    /**
     * 构造函数
     * @param scene 画面渲染场景
     * @param env   jni环境
     * @param audioTrackUtil 音频播放工具
     */
    FFPlayerImpl(
            Section902 *scene,
            JNIEnv *env,
            jobject audioTrackUtil
    ) {
        this->scene = scene;
        this->jniEnv = env;
        //在jni中，jobject需要在不同线程访问的话，需要将其转成全局引用，我们在析构函数中在将其全局引用删除
        this->globalAudioTrackUtil = env->NewGlobalRef(audioTrackUtil);
        //输出一条日志
        LOGI("init player");
    }

    ~FFPlayerImpl() {
        //对象销毁前，我们先把缓冲队列的音视频数据包释放掉
        //缓冲队列通过new动态分配的，需要通过delete来删除对象，c会调用它的析构函数。
        AVPacket *avPacket;
        while (!videoPackets->empty()) {
            videoPackets->pop(avPacket);
            av_packet_unref(avPacket);
            av_packet_free(&avPacket);
        }
        delete videoPackets;
        while (!audioPackets->empty()) {
            audioPackets->pop(avPacket);
            av_packet_unref(avPacket);
            av_packet_free(&avPacket);
        }
        delete audioPackets;
        this->jniEnv->DeleteGlobalRef(globalAudioTrackUtil);
        this->scene = nullptr;
        this->jniEnv = nullptr;
        this->globalAudioTrackUtil = nullptr;
    }

    int setMedia(const char *url) override {
        mediaUrl = url;
        startPlayTime = getCurrTime();
        LOGI("setMedia: %s", url);
        prepare();
        return 0;
    }

    int play() override {
        playing = 1;
        return 0;
    }

    int pause() override {
        playing = 0;
        return 0;
    }

    int seek(long timeStamp) override {
        return 0;
    }

    int stop() override {
        playerState = 0;
        return 0;
    }

    int reset() override {
        return 0;
    }

private:
    Section902 *scene;
    JNIEnv *jniEnv;
    jobject globalAudioTrackUtil;
    //视频时长
    long duration = 0;
    //播放进度
    long progress = 0;
    //播放器状态(0 空闲，1准备资源，2播放结束，3播放器错误)
    int playerState = 0;
    //播放中
    int playing = 0;
    //记录开始播放的时间
    long startPlayTime = 0;
    //媒体文件路径
    const char *mediaUrl = nullptr;

    //解封装线程
    std::thread avFormatThread;
    //视频解码线程
    std::thread videoDecodeThread;
    //音频解码线程
    std::thread audioDecodeThread;

    //拉流解封装上下文
    AVFormatContext *avFormatContext = nullptr;
    //缓冲封装包，音频、视频最多缓冲10个包
    BlockQueue<AVPacket *> *videoPackets = new BlockQueue<AVPacket *>(20);
    BlockQueue<AVPacket *> *audioPackets = new BlockQueue<AVPacket *>(20);

    //音视编解码上下文
    AVCodecContext *videoCodecContext = nullptr;
    AVCodecContext *audioCodecContext = nullptr;
    //音视频的时间基数，dts pts乘以这个值将会得到这一帧数据在媒体时长中的位置，单位秒
    double videoRationalDouble = 0.0;
    double audioRationalDouble = 0.0;


    int prepare() {
        playerState = 1;
        avFormatThread = std::thread(&FFPlayerImpl::startPrepareAndBuffer, this);
        if (!avFormatThread.joinable()) {
            LOGE("can\'t create avFormat handle thread");
            return -1;
        }
        return 0;
    }

    /**
     * 开始准备：流加载、解封装、准备解码器，预加载缓冲视频包
     */
    void startPrepareAndBuffer() {
        //avformat 是处理音视频文件封装格式的，申请上下文用于解封装，读取文件头
        avFormatContext = avformat_alloc_context();
        int ret = avformat_open_input(&avFormatContext, mediaUrl, nullptr, nullptr);
        if (ret < 0) {
            LOGE("can\'t open input %d", ret);
            return;
        }
        //打印读取到的媒体时长
        duration = avFormatContext->duration;
        LOGI("video duration: %" PRId64, duration);
        //读取流信息，特别是针对一些没有文件头的格式，通过此方法可以计算帧率等
        ret = avformat_find_stream_info(avFormatContext, nullptr);
        if (ret < 0) {
            LOGE("can\'t find stream info");
            return;
        }
        //解封装后从视频封装格式中分离出视频流与音频流
        int video_index = -1;
        int audio_index = -1;
        for (int i = 0; i < avFormatContext->nb_streams; i++) {
            //根据流类型找到视频流
            if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
                video_index = i;
            if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
                audio_index = i;
        }
        if (video_index == -1 || audio_index == -1) {
            //没有视频流
            LOGE("can\'t find video stream");
            return;
        }
        LOGI("createAVCodecContext");
        //初始化解码器上下文，如果找不到解码器直接返回
        videoCodecContext = createAVCodecContext(avFormatContext->streams[video_index]->codecpar);
        if (videoCodecContext == nullptr) {
            return;
        }
        videoRationalDouble = av_q2d(avFormatContext->streams[video_index]->time_base);
        audioCodecContext = createAVCodecContext(avFormatContext->streams[audio_index]->codecpar);
        if (audioCodecContext == nullptr) {
            return;
        }
        audioRationalDouble = av_q2d(avFormatContext->streams[audio_index]->time_base);
        //初始化音视频解码线程
        videoDecodeThread = std::thread(&FFPlayerImpl::decodeVideo, this);
        audioDecodeThread = std::thread(&FFPlayerImpl::decodeAudio, this);
        if (!videoDecodeThread.joinable() || !audioDecodeThread.joinable()
                ) {
            LOGE("can\'t create av decode thread");
            return;
        }

        //申请包缓冲区packet
        AVPacket *avPacket = av_packet_alloc();
        //拉流解封装
        while (playerState == 1) {
            if (av_read_frame(avFormatContext, avPacket) >= 0) {
                if (avPacket->stream_index == video_index) {
                    //视频包处理
                    videoPackets->push_back(avPacket);
                    avPacket = av_packet_alloc();
                } else if (avPacket->stream_index == audio_index) {
                    //音频包处理
                    audioPackets->push_back(avPacket);
                    avPacket = av_packet_alloc();
                }
            }
        }
        LOGE("End Play");
        av_packet_free(&avPacket);
        avcodec_close(audioCodecContext);
        avcodec_close(videoCodecContext);
        avformat_close_input(&avFormatContext);
        playerState = 2;
    }

    /**
     * 创建AVCodecContext，内部已经初始化
     * @param pParameters 编码参数
     * @return 如果创建失败将返回空
     */
    AVCodecContext *createAVCodecContext(AVCodecParameters *pParameters) {
        //avcodec主要工作是音视频解码，这里申请一个解码对象
        AVCodecContext *avCodecContext = avcodec_alloc_context3(nullptr);
        //设置编解码参数，从解封装avFormatContext对象的视频流里面拿
        LOGD("avcodec_parameters_to_context");
        avcodec_parameters_to_context(avCodecContext, pParameters);
        //根据参数得到视频解码类型，查找解码器
        LOGD("avcodec_find_decoder");
        const AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);
        if (avCodec == nullptr) {
            LOGE("can\'t find decoder");
            return nullptr;
        } else {
            LOGE("find decoder %s %s", avCodec->name,
                 avCodec->wrapper_name);
        }
        //根据编解码，初始化avCodecContext
        int ret = avcodec_open2(avCodecContext, avCodec, nullptr);
        if (ret < 0) {
            LOGE("can\'t open audio decoder %d", ret);
            return nullptr;
        }
        return avCodecContext;
    }

    /**
     * 解码音频
     */
    void decodeAudio() {
        //在子线程中调用jni方法，比如GetObjectClass等，需要先将子线程附加到jvm才可以，否则会报错 a thread (tid 25212) is making JNI calls without being attached in call to GetObjectClass
        //使用的到的jobject对象需要全局引用才能跨线程访问（外部创建的一般在主线程，这里是单独线程解码音频）
        JNIEnv *threadEnv;//每个线程都有自己的jni环境，这里声明个指针
        auto *javaVm = (JavaVM *) (av_jni_get_java_vm(
                nullptr));//这里我们直接从ffmpeg取得jvm，它在JNI_OnLoad函数中赋值
        jint result = javaVm->AttachCurrentThread(&threadEnv, nullptr);//将当前线程附加到jvm，并取得当前线程的jni环境
        if (result != JNI_OK) {
            // 处理附加失败的情况
            LOGE("Attach jvm failed");
            return;
        }
        if (threadEnv == nullptr) {
            LOGE("threadEnv is null");
        }
        //底层调用java，类似反射，取得java类，取得他的方法签名，然后调用该方法传入目标对象
        //据说jni调用如果数据量大的话会有性能问题，不能使用这种传统的数据传递，这里只是传输pcm数据，应该还不至于
        //获取到class对象
        jclass david_player = threadEnv->GetObjectClass(globalAudioTrackUtil);
        //从class获取method，签名表达方式：小括号内表示参数类型，[表示数组，B表示Byte,I表示Int。后面的值表示返回值(V表示无返回值Void)
        //AudioTrack用于播放音频，由java层创建，这里的API用于创建音频播放器、持续写入播放数据、销毁音频播放器
        jmethodID createAudio = threadEnv->GetMethodID(david_player, "createAudioTrack", "(II)V");
        jmethodID audio_write = threadEnv->GetMethodID(david_player, "playBuffer", "([BI)V");
        jmethodID destroyAudio = threadEnv->GetMethodID(david_player, "destroyAudioTrack", "()V");

        //标准音频转换，解码后是pcm，我们这里统一转换（音频重采样）
        // 采样率44100Hz，常见的采样率是22.05KHz、44.1KHz、48KHz，不知道会不会有的音频不是这个采样率，更不知道会不会有的手机不支持导致audioTrack播放失败
        // 双声道(像杜比音效那些可能会有多个声道，但安卓手机一般只支持单声道或者双声道（外放+听筒）)
        // 位宽16bit（常见的也有8bit的，但统一转8bit会导致声音精度下降）
        SwrContext *swrContext = swr_alloc();//申请一个转码上下文
        int out_sample_rate = 44100;//输出采样率
        long out_ch_layout = AV_CH_LAYOUT_STEREO;//声道布局，双声道（左前方、右前方）
        int out_nb_channels = av_get_channel_layout_nb_channels(out_ch_layout);//声道数量，这里当然就是两个了
        enum AVSampleFormat out_format = AV_SAMPLE_FMT_S16;//采样格式 16bit
        LOGI("src audio config %ld,%d,%d,%d", audioCodecContext->channel_layout,
             audioCodecContext->sample_fmt, audioCodecContext->sample_rate,
             av_get_channel_layout_nb_channels(audioCodecContext->channel_layout));
        LOGI("dst audio config %ld,%d,%d,%d", out_ch_layout,
             out_format, out_sample_rate, out_nb_channels);
        //音频重采样参数设置
        swr_alloc_set_opts(
                swrContext,
                out_ch_layout,
                out_format,
                out_sample_rate,
                audioCodecContext->channel_layout,//音频流的声道布局，如果它是上下的声道，能够在空间上智能转换成左右吗？
                audioCodecContext->sample_fmt,//音频流的采样格式
                audioCodecContext->sample_rate,//音频流的采样率
                0,
                nullptr
        );
        swr_init(swrContext);//初始化重采样上下文
        uint8_t *out_v_buffer = (uint8_t *) av_malloc(
                out_sample_rate * 2 * out_nb_channels);//申请缓冲区，这里申请1秒的数据量，每帧音频16bit即2个字节，声道数

        //Java层创建AudioTrack
        threadEnv->CallVoidMethod(globalAudioTrackUtil, createAudio, out_sample_rate,
                                  out_nb_channels);
        //申请帧缓冲区frame
        AVPacket *avPacket;
        AVFrame *avFrame = av_frame_alloc();
        int ret;
        int out_nb_samples;//重采样后每个声道的样本数量
        while (playerState == 1) {
            audioPackets->pop(avPacket);//阻塞至拿到音频数据包
            ret = avcodec_send_packet(audioCodecContext, avPacket);//发送数据包到音频解码上下文，根据返回值可以判断是否支持解码
            if (ret == 0) {
                //循环解码得到音频帧
                while ((ret = avcodec_receive_frame(audioCodecContext, avFrame)) == 0) {
                    //音频重采样，标准化输出
                    out_nb_samples = swr_convert(swrContext,
                                                 &out_v_buffer, //输出的缓冲区，这个必须足够大，若不足以容纳一帧的样本数量会导致丢失
                                                 out_sample_rate,//缓冲区，每个声道可以容纳的最大样本数量，我们的缓冲区是1秒的数据里，这里就是采样率了
                                                 (const uint8_t **) avFrame->data, //音频帧数据
                                                 avFrame->nb_samples//每个声道的样本数量，这里打印出来是1024，不同的音频文件，这个音频帧大小可能不同？，同一音频文件里面的音频帧大小是否也有可能会有变化
                    );
                    if (out_nb_samples < 0) {
                        break;
                    }
                    //计算重采样后音频数据的字节数，依据此大小创建对应的java字节数组，1024*2个声道*2byte(16bit)=4096
                    int size = av_samples_get_buffer_size(nullptr,
                                                          out_nb_channels,
                                                          out_nb_samples,
                                                          out_format,
                                                          1);
                    jbyteArray audio_sample_array = threadEnv->NewByteArray(size);
                    //将uint8_t *强转jbyte *，这是底层的数组用指针表示，我们将其转复制到java字节数组
                    threadEnv->SetByteArrayRegion(audio_sample_array, 0, size,
                                                  (const jbyte *) out_v_buffer);
                    //将音频数据发送到java层，通过AudioTrack进行播放
                    threadEnv->CallVoidMethod(globalAudioTrackUtil, audio_write, audio_sample_array,
                                              size);
                    //销毁生成的java数组（这个东西，每一帧数据是否都是一样的长度呢，如果是，那我是不是能够复用）
                    threadEnv->DeleteLocalRef(audio_sample_array);
                }
            }
            av_packet_unref(avPacket);//清除数据包数据
            av_packet_free(&avPacket);//释放数据包引用
            //解码异常
            if (ret < 0 && ret != AVERROR(EAGAIN)) {//EAGAIN，可能是这一包数据所有帧已经读取完，读取下个数据包
                LOGE("PLAYER ERROR BY audio");
                break;
            }
        }
        //销毁AudioTrack
        threadEnv->CallVoidMethod(globalAudioTrackUtil, destroyAudio);

        swr_free(&swrContext);//释放重采样上下文
        av_free(out_v_buffer);//释放重采样缓冲区
        av_frame_free(&avFrame);//释放音频帧

        //将线程与jvm解绑
        javaVm->DetachCurrentThread();
    }

    /**
     * 解码视频
     */
    void decodeVideo() {
        //标准视频帧转换
        AVPixelFormat avPixelFormat = videoCodecContext->pix_fmt;//当前视频解码后的像素格式
        int vWidth = videoCodecContext->width;//视频宽度（像素）
        int vHeight = videoCodecContext->height;//视频高度（像素）

        //目前用的比较多的像素格式是yuv420，它在特效处理、图像识别、存储空间上有优势，但说不准有的视频不是这个像素格式，那就需要转换
        AVFrame *yuv420pFrame = av_frame_alloc();//申请一个视频帧
        int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, vWidth, vHeight,
                                                   1);//根据像素格式和视频宽高计算缓冲区大小
        uint8_t *out_buffer = (uint8_t *) av_malloc(buffer_size * sizeof(uint8_t));//申请空间用于视频帧数据缓冲区
        av_image_fill_arrays(//初始化视频帧
                yuv420pFrame->data,
                yuv420pFrame->linesize,
                out_buffer,
                AV_PIX_FMT_YUV420P,
                vWidth,
                vHeight,
                1
        );
        //若当前帧像素格式不是YUV420P，可以通过转码上下文实现转换，它主要用于图像缩放与像素格式转换
        SwsContext *swsContext = sws_getContext(
                vWidth, vHeight, videoCodecContext->pix_fmt,
                vWidth, vHeight, AV_PIX_FMT_YUV420P,
                SWS_BICUBIC, nullptr, nullptr, nullptr);

        //申请帧缓冲区frame
        AVPacket *avPacket;
        AVFrame *avFrame = av_frame_alloc();
        double showTime, sleepTime;
        long currTime;
        int ret;
        LOGI("startPlayTime %ld", startPlayTime);
        while (playerState == 1) {
            videoPackets->pop(avPacket);//从队列取出一包数据，这里阻塞到解码线程往队列放入数据才唤醒
            ret = avcodec_send_packet(videoCodecContext, avPacket);//将数据包发送到解码器上下文
            if(ret == 0){
                //一包数据通常可以解出多个视频帧，我们从解码器上下文循环读取
                while ((ret = avcodec_receive_frame(videoCodecContext, avFrame)) == 0) {
                    //计算视频帧的显示时间，阻塞线程到目标时间进行播放
                    //若是网络视频缓冲或者暂停播放，这里会有问题，时间轴应该考虑数据包阻塞时间、暂停阻塞时间
                    showTime = avFrame->pts * videoRationalDouble;
                    currTime = getCurrTime();
                    sleepTime = showTime * 1000 - (currTime - startPlayTime);
                    //线程睡眠 微秒
                    LOGI("sleepTime %lf", sleepTime);
                    usleep(sleepTime * 1000);
                    //转换成yuv420p格式
                    if (avPixelFormat == AV_PIX_FMT_YUV420P) {
                        sendToScreen(avFrame, vWidth, vHeight);
                    } else {//当前像素格式并非yuv420p，需要进行一次转换
                        ret = sws_scale(
                                swsContext,
                                (const uint8_t *const *) avFrame->data,
                                avFrame->linesize,
                                0,
                                avFrame->height,
                                yuv420pFrame->data,
                                yuv420pFrame->linesize
                        );
                        if (ret < 0) {
                            LOGE("player error 4");
                        }
                        sendToScreen(yuv420pFrame, vWidth, vHeight);
                    }
                }
            }
            av_packet_unref(avPacket);//清除数据包数据
            av_packet_free(&avPacket);//释放数据包引用
            if (ret < 0 && ret != AVERROR(EAGAIN)) {
                //判断一下解码器是否正常工作，若为0表示成功，其他情况皆是出现异常
                LOGE("PLAYER ERROR");
                break;
            }
        }
        sws_freeContext(swsContext);//释放像素转换上下文
        av_free(out_buffer);//释放像素转换缓冲区
        av_frame_free(&avFrame);//释放视频帧
        av_frame_free(&yuv420pFrame);//释放像素转换视频帧
    }

    /**
     * 获取当前系统时间
     */
    long getCurrTime() {
        return std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::system_clock::now().time_since_epoch()).count();
    }

    /**
      * 将画面发送到屏幕显示
      * @param avFrame   视频帧（像素格式要先转换为yuv420，opengl实现的画面渲染，做滤镜效果的话用yuv实现比较）
      * @param width     视频宽度（像素）
      * @param height    视频高度（像素）
      */
    void sendToScreen(AVFrame *avFrame, int width, int height) {
        //申请内存
        auto *y = new unsigned char[width * height];
        auto *u = new unsigned char[width * height / 4];
        auto *v = new unsigned char[width * height / 4];
        memcpy(y, avFrame->data[0], width * height);
        memcpy(u, avFrame->data[1], width * height / 4);
        memcpy(v, avFrame->data[2], width * height / 4);
        scene->setPictureData(width, height, y, u, v);
    }

};